<?php

/*

PhpTest: a simple class for producing test suites for PHP code,
modelled after tcltest(n).

TODO: implementation is not complete yet.

*/

# TODO: when PHP 5.3 will be out use a namespace.
$PhpTest_options = array(
  # TODO: this list is not complete yet.
  'verbose' => 'body error'
);  // const

class PhpTest {
  # Defines and runs a test. Returns an empty string.
  public static function test($name, $description, $options, $result = NULL) {
    global $PhpTest_options;

    /*
      Support for a simpler interface for a
      common usage alike tcltest version 1.
     */
    if (! is_array($options)) {
      $body = $options;
      $options = array();
    } else {
      $body = $options['body'];
      $result = $options['result'];
    }

    if ($body[strlen($body)-1] != ';') {
      $body .= ';';
    }

    /*
      If there is a parse error in the evaluated code, eval() returns FALSE and
      execution of the following code continues normally.
      It is not possible to catch a parse error in eval() using
      set_error_handler().
      In case of a fatal error in the evaluated code, the whole script exits.
      (extracted from eval PHP manual page).

      Thus when actual_result is FALSE and result is FALSE too
      there is no way to know if an error has occurred or not
      by only comparing actual_result with result.
      Heuristic: compare last error before and after eval.
     */
    $error_before = error_get_last();
    $actual_result = @eval($body);
    $error_after = error_get_last();

    $correct = $error_before === $error_after;
    $pass = $correct && $actual_result === $result;

    if ($pass) {
      if (self::option('verbose', 'pass')) {
        echo "++++ $name PASSED\n";
      }
    } else {
      echo "\n\n==== $name $description FAILED\n";
      if (self::option('verbose', 'body')) {
        echo "==== Contents of test case:\n$body\n";
      }

      if ($correct) {
         echo <<<EOT
---- Result was:
$actual_result
---- Result should have been (exact matching):
$result

EOT;
      } else {
        echo "---- Test generated error: ";
        # Error level description. The same used by PHP.
        switch ($error_after['type']) {
          case E_NOTICE:
          case E_USER_NOTICE:
            $level = 'Notice';
          break;

          case E_WARNING:
          case E_USER_WARNING:
            $level = 'Warning';
          break;

          case E_USER_ERROR:
            $level = 'Fatal error';
          break;

          case E_STRICT:
            $level = 'Strict Standards';
          break;

          case E_RECOVERABLE_ERROR:
            $level = 'Catchable fatal error';
          break;

          case E_PARSE:
            $level = 'Parse error';
          break;
          # There should not be other cases left over.
        }
        printf("%s:  %s in %s on line %u", $level, $error_after['message'],
          $error_after['file'], $error_after['line']);
        echo "\n---- Backtrace:\n";
        debug_print_backtrace();
      }

      echo "==== $name FAILED\n\n";
    }
  }

  protected static function option($option, $value) {
    global $PhpTest_options;

    return strpos($PhpTest_options[$option], $value) !== FALSE;
  }

  public static function configure($options = NULL) {
    global $PhpTest_options;

    if (is_null($options)) {
      return implode(' ', array_keys($PhpTest_options));
    }

    if (is_string($options)) {
      return $PhpTest_options[$options];
    }

    foreach ($options as $option => $value) {
      if (array_key_exists($option, $PhpTest_options)) {
        $PhpTest_options[$option] = $value;
      } else {
        # TODO: preg_replace last ", " with ", or"
        trigger_error(sprintf('unknown option %s: should be one of %s', 
          $option, implode(', ', array_keys($PhpTest_options))), E_USER_ERROR);
      }
    }
  }

  public static function verbose($level = NULL) {
    global $PhpTest_options;

    if (is_null($level)) {
      return $PhpTest_options['verbose'];
    }

    $PhpTest_options['verbose'] = $level;
  }
}

# End of file
# vim: set ts=2 sw=2 et:
?>
